package edu.usm.predatorwars;

import java.awt.Point;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;

import edu.predatorwars.shared.models.Board;
import edu.predatorwars.shared.models.BoardPayload;
import edu.predatorwars.shared.models.CharacterListPayload;
import edu.predatorwars.shared.models.Creature;
import edu.predatorwars.shared.models.GameInfo;
import edu.predatorwars.shared.models.GameListPayload;
import edu.predatorwars.shared.models.Location;
import edu.predatorwars.shared.models.Message;
import edu.predatorwars.shared.models.MessageType;
import edu.predatorwars.shared.models.Move;
import edu.predatorwars.shared.models.Player;
import edu.predatorwars.shared.models.StateChangePayload;
import edu.predatorwars.shared.models.Terrain;

/**
 * This is the primary class a client should interact with.
 * The expected sequence of events are:
 * 0. (Optional) Create game(s) {@link #createGame()}
 * 1. Get a listing for all games
 * 2. Join a game {@link #joinGame(UUID, Playable)}
 * 3. Select a character
 * 4. Use either makeMove or quit game {@link #quitGame(UUID)}
 */
public class ClientMask {
	
	private final GameHostResourceClient gameHostClient;
	private final Player player;
	private final GameEventConsumer gameEventConsumer;
	private final GameEventProducer gameEventProducer;
	
	private boolean isGameOver;

	public ClientMask(GameEventProducer producer, GameEventConsumer gameEventConsumer, GameHostResourceClient gamehost, Player player) {
		this.gameEventProducer = producer;
		this.gameEventConsumer = gameEventConsumer;
		this.gameHostClient = gamehost;
		this.player = player;
	}
	
	public void createGame(Playable playable) {
		gameEventProducer.start();
		if (gameHostClient.createGame(player)) {
			startGameLoop(playable);
		}
	}
	
	public List<GameInfo> refreshGameList() {
		GameListPayload payload = gameHostClient.refreshGameList();
		if (payload == null) {
			return Collections.emptyList();
		}
		
		return payload.getGameList();
	}

	public void joinGame(UUID gameId, Playable playable) {
		gameEventProducer.start();
		gameHostClient.joinGame(player, gameId);
		
		startGameLoop(playable);
	}

	private void startGameLoop(Playable playable) {
		isGameOver = false;
		while(!isGameOver) {
			handleGameEvent(playable);
		}
	}

	public void handleGameEvent(Playable playable) {
		Message msg = gameEventConsumer.nextGameEvent();

		if (msg != null) {
			processMessage(msg, playable);
		}
	}
	
	public void quitGame(UUID gameId) {
		isGameOver = true;
		gameHostClient.quitGame(player, gameId);
	}
	
	public Player getPlayer() {
		return player;
	}
	
	private void processMessage(Message msg, Playable playable) {
		if (msg.getType().equals(MessageType.BoardChange)) {
			BoardPayload payload =(BoardPayload)msg;
			Creature pet = player.getCreature();
			Set<Move> possibleMoves = new TreeSet<Move>();
			Location spot = null;
			int myX = -1;
			int myY = -1;
			Location tempPlayerLocation;
			Board field = payload.getBoard();
			//Loops through the board to find the location of the player's creature.

			for(int i = 0; i<field.getWidth(); i++){
				for(int j = 0; j<field.getHeight(); j++){
					tempPlayerLocation = field.getLocation(i, j);
					if(tempPlayerLocation.getPlayer() != null 
							&& tempPlayerLocation.getPlayer().equals(player)){
						spot = tempPlayerLocation;
						myX = i;
						myY = j;
						j = field.getHeight();
						i = field.getWidth();
					}
				}
			}
			
			if(payload.getWhoseTurn().equals(player)){
				if(null != spot){
					int xLow =  myX - 1;
					if (xLow < 0) {
						xLow = 0;
					}
					int xHigh = myX + 1;
					if (xHigh >= field.getWidth()) {
						xHigh = myX;
					}
					int yLow =  myY - 1;
					if (yLow < 0) {
						yLow = 0;
					}
					int yHigh = myY + 1;
					if (yHigh >= field.getHeight()) {
						yHigh = myY;
					}
					
					//Checks to which locations within the player's sight that
					//constitute a valid move
					for (int y = yLow; y <= yHigh; y++) {
						if (isPossibleMove(myX, y, field.getLocation(myX, y))) {
							possibleMoves.add(new Move(myX, y));
						}
					}
					
					for(int x = xLow; x <= xHigh; x++) {
						if (isPossibleMove(x, myY, field.getLocation(x, myY))) {
							possibleMoves.add(new Move(x, myY));
						}
					}
				}
			} 

			Point origin = new Point(myX, myY);
			filterBoard(payload.getBoard(), origin, pet);
			playable.onBoardUpdate(payload.getBoard(), payload.getPlayerList(), myX, myY);
			
			if (possibleMoves.size() > 0) {
				Move move = playable.onMove(possibleMoves, myX, myY);
				gameHostClient.makeMovement(player, move, payload.getGameId(), payload.getBoardSnapshot());
			}
		} else if (msg.getType().equals(MessageType.StateChange)) {
			StateChangePayload payload = (StateChangePayload) msg;
			handleStateChange(payload, playable);
		} else if (msg.getType().equals(MessageType.CharacterList)) {
			CharacterListPayload payload = (CharacterListPayload) msg;
			Creature creature = playable.onSelectCreature(Arrays.asList(payload.getCharacterList()));
			player.setCreature(creature);
			gameHostClient.selectCharacter(player, creature, payload.getGameId());
		}
	}
	
	private void filterBoard(Board board, Point origin, Creature creature) {
		hideLocationsNotInSight(board.getLocations(), origin, creature.getStat(Creature.STAT_SIGHT));
		checkAdjacentSpots(board.getLocations(), origin);
		checkDiagonalSpots(board.getLocations(), origin);
	}

	private void hideLocationsNotInSight(Location[][] locations, Point origin, float stat) {
		int sight = (int) Math.floor(stat);
		int yHigh = getMaxInSequence(origin.y, sight, locations[0].length);
		int xHigh = getMaxInSequence(origin.x, sight, locations.length);
		int yLow = getMinInSequence(origin.y, sight, 0);
		int xLow = getMinInSequence(origin.x, sight, 0);
		for(int i = 0; i < locations.length; i++) {
			for(int j = 0; j < locations[0].length; j++) {
				if (i < xLow || i > xHigh || j < yLow || j > yHigh) {
					locations[i][j].nullify();
				}
			}
		}
	}

	private void checkAdjacentSpots(Location[][] locationsInSight, Point origin) {
		checkAdjacentOnX(locationsInSight, origin, false);
		checkAdjacentOnX(locationsInSight, origin, true);
		checkAdjacentOnY(locationsInSight, origin, false);
		checkAdjacentOnY(locationsInSight, origin, true);
	}

	
	
	private void checkAdjacentOnX(Location[][] locationsInSight, Point origin, boolean goLeft) {
		boolean shouldNull = false;
		int startX = (goLeft ? origin.x - 1 : origin.x + 1);
		for(int i = startX; (goLeft ? i >= 0 : i < locationsInSight.length);) {
			if (locationsInSight[i][origin.y] == null) {
				break;
			} else if (shouldNull) {
				locationsInSight[i][origin.y].nullify();
			}
			else if (locationsInSight[i][origin.y].getTerrain().isSightBlocked()) {
				shouldNull = true;
			}
			
			i = (goLeft ? i - 1 : i + 1);
		}
	}

	private void checkAdjacentOnY(Location[][] locationsInSight, Point origin, boolean down) {
		boolean shouldNull = false;
		int startY = (down ? origin.y - 1 : origin.y + 1);
		for(int i = startY; (down ? i >= 0 : i < locationsInSight[0].length);) {
			if (locationsInSight[origin.x][i] == null) {
				break;
			} else if (shouldNull) {
				locationsInSight[origin.x][i].nullify();
			} else if (locationsInSight[origin.x][i].getTerrain().isSightBlocked()) {
				shouldNull = true;
			}
			
			i = (down ? i - 1 : i + 1);
		}
	}
	
	
	private void checkDiagonalSpots(Location[][] locationsInSight, Point origin) {
		checkQuadrantOne(locationsInSight, origin);
		checkQuadrantTwo(locationsInSight, origin);
		checkQuadrantThree(locationsInSight, origin);
		checkQuadrantFour(locationsInSight, origin);
	}

	private void checkQuadrantOne(Location[][] locationsInSight, Point origin) {
		Point nextDiagonal = nextDiagonal(origin, true, true);
		while(nextDiagonal.x < locationsInSight.length && nextDiagonal.y < locationsInSight[0].length) {
			if (locationsInSight[nextDiagonal.x][nextDiagonal.y] == null) {
				break;
			} else if (locationsInSight[nextDiagonal.x][nextDiagonal.y].getTerrain().isSightBlocked()) {
				nullSquareArea(locationsInSight, nextDiagonal, true, true);
				break;
			}
			nextDiagonal = nextDiagonal(nextDiagonal, true, true);
		}
	}
	
	
	private void checkQuadrantTwo(Location[][] locationsInSight, Point origin) {
		Point nextDiagonal = nextDiagonal(origin, false, true);
		while(nextDiagonal.x >= 0 && nextDiagonal.y < locationsInSight[0].length) {
			if (locationsInSight[nextDiagonal.x][nextDiagonal.y] == null) {
				break;
			} else if (locationsInSight[nextDiagonal.x][nextDiagonal.y].getTerrain().isSightBlocked()) {
				nullSquareArea(locationsInSight, nextDiagonal, false, true);
				break;
			}
			nextDiagonal = nextDiagonal(nextDiagonal, false, true);
		}		
	}


	private void checkQuadrantThree(Location[][] locationsInSight, Point origin) {
		Point nextDiagonal = nextDiagonal(origin, false, false);
		while(nextDiagonal.x >= 0 && nextDiagonal.y >= 0) {
			if (locationsInSight[nextDiagonal.x][nextDiagonal.y] == null) {
				break;
			} else if (locationsInSight[nextDiagonal.x][nextDiagonal.y].getTerrain().isSightBlocked()) {
				nullSquareArea(locationsInSight, nextDiagonal, false, false);
				break;
			}
			nextDiagonal = nextDiagonal(nextDiagonal, false, false);
		}				
	}	
	

	private void checkQuadrantFour(Location[][] locationsInSight, Point origin) {
		Point nextDiagonal = nextDiagonal(origin, true, false);
		while(nextDiagonal.x < locationsInSight.length && nextDiagonal.y >= 0) {
			if (locationsInSight[nextDiagonal.x][nextDiagonal.y] == null) {
				break;
			} else if (locationsInSight[nextDiagonal.x][nextDiagonal.y].getTerrain().isSightBlocked()) {
				nullSquareArea(locationsInSight, nextDiagonal, true, false);
				break;
			}
			nextDiagonal = nextDiagonal(nextDiagonal, true, false);
		}				
	}
	
	private Point nextDiagonal(Point origin, boolean right, boolean up) {
		int nextX = (right ? origin.x + 1 : origin.x - 1);
		int nextY = (up ? origin.y + 1: origin.y - 1);
		return new Point(nextX, nextY);
	}

	private void nullSquareArea(Location[][] locs, Point nextDiagonal, boolean right, boolean up) {
		int startX = (right ? nextDiagonal.x : 0);
		int startY = (up ? nextDiagonal.y : 0);
		int endX = (right ? locs.length : nextDiagonal.x + 1);
		int endY = (up ? locs[0].length : nextDiagonal.y + 1);
		
		for(int i = startX; i < endX; i++) {
			for(int j = startY; j < endY; j++) {
				if (i == nextDiagonal.x && j == nextDiagonal.y) {
					continue;
				}
				locs[i][j].nullify();
			}
		}
	}	
	

	private int getMaxInSequence(int start, int distance, int max) {
		int end = start;
		while(distance > 0 && end < max) {
			end += 1;
			distance -= 1;
		}
		
		return end;
	}
	
	private int getMinInSequence(int start, int distance, int min) {
		int end = start;
		while(distance > 0 && end > min) {
			end -= 1;
			distance -= 1;
		}
		return end;
	}

	private boolean isPossibleMove(int x, int y, Location tempLocation) {
		boolean possible = false;
		if(!tempLocation.getTerrain().isSightBlocked()){
			if(tempLocation.getTerrain().isTraversable()){
				possible = true;
			}
		}
		
		return possible;
	}

	private void handleStateChange(StateChangePayload payload, Playable playable) {
		switch(payload.getGameState()) {
			case 1: // create selection
				playable.onGameLoadingState(payload.getPlayerList());
				break;
			case 2: // game started
				playable.onGameStartState(payload.getPlayerList());
				break;
			case 3: // game ended
				playable.onGameEndState(payload.getPlayerList());
				break;
		}
	}

}
